/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.explorer.ui.management.crystalball;

import com.vaadin.data.Item;
import com.vaadin.data.Property;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.Property.ValueChangeListener;
import com.vaadin.ui.*;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.themes.Reindeer;
import org.activiti.crystalball.simulator.*;
import org.activiti.crystalball.simulator.delegate.event.Function;
import org.activiti.crystalball.simulator.delegate.event.impl.EventLogProcessInstanceCreateTransformer;
import org.activiti.crystalball.simulator.delegate.event.impl.EventLogTransformer;
import org.activiti.crystalball.simulator.delegate.event.impl.EventLogUserTaskCompleteTransformer;
import org.activiti.crystalball.simulator.impl.StartReplayLogEventHandler;
import org.activiti.crystalball.simulator.impl.replay.ReplayUserTaskCompleteEventHandler;
import org.activiti.engine.*;
import org.activiti.engine.event.EventLogEntry;
import org.activiti.engine.history.HistoricProcessInstance;
import org.activiti.engine.impl.el.NoExecutionVariableScope;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.explorer.ExplorerApp;
import org.activiti.explorer.I18nManager;
import org.activiti.explorer.Messages;
import org.activiti.explorer.ui.custom.DetailPanel;
import org.activiti.explorer.ui.mainlayout.ExplorerLayout;
import org.activiti.explorer.ui.variable.VariableRendererManager;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Tijs Rademakers
 */
public class EventOverviewPanel extends DetailPanel {

    private static final long serialVersionUID = 1L;

    // Process instance start event
    private static final String PROCESS_INSTANCE_START_EVENT_TYPE = "PROCESS_INSTANCE_START";
    private static final String PROCESS_DEFINITION_ID_KEY = "processDefinitionId";
    private static final String VARIABLES_KEY = "variables";
    // User task completed event
    private static final String USER_TASK_COMPLETED_EVENT_TYPE = "USER_TASK_COMPLETED";

    private static final String SIMULATION_BUSINESS_KEY = "testBusinessKey";

    protected transient HistoryService historyService;
    protected transient RepositoryService repositoryService;
    protected transient RuntimeService runtimeService;
    protected transient IdentityService identityService;
    protected transient ManagementService managementService;
    protected I18nManager i18nManager;
    protected VariableRendererManager variableRendererManager;

    protected HorizontalLayout instanceLayout;
    protected NativeSelect definitionSelect;
    protected Button replayButton;
    protected Table instanceTable;
    protected HorizontalLayout eventLayout;
    protected Button stepButton;
    protected Button showProcessInstanceButton;
    protected Table eventTable;
    protected Label noMembersTable;

    protected List<ProcessDefinition> definitionList;
    protected Map<String, ProcessDefinition> definitionMap = new HashMap<String, ProcessDefinition>();
    protected List<HistoricProcessInstance> instanceList;
    protected List<SimulationEvent> simulationEvents;
    protected HistoricProcessInstance replayHistoricInstance;
    protected SimulationDebugger simulationDebugger;

    public EventOverviewPanel() {
        this.runtimeService = ProcessEngines.getDefaultProcessEngine().getRuntimeService();
        this.historyService = ProcessEngines.getDefaultProcessEngine().getHistoryService();
        this.repositoryService = ProcessEngines.getDefaultProcessEngine().getRepositoryService();
        this.identityService = ProcessEngines.getDefaultProcessEngine().getIdentityService();
        this.managementService = ProcessEngines.getDefaultProcessEngine().getManagementService();
        this.variableRendererManager = ExplorerApp.get().getVariableRendererManager();
        this.definitionList = repositoryService.createProcessDefinitionQuery().orderByProcessDefinitionName().asc().list();
        this.instanceList = historyService.createHistoricProcessInstanceQuery().orderByProcessInstanceStartTime().desc().list();
        this.i18nManager = ExplorerApp.get().getI18nManager();

        initializeDefinitionMap();
        init();
        initializeCurrentValues();
    }

    protected void initializeDefinitionMap() {
        for (ProcessDefinition definition : definitionList) {
            definitionMap.put(definition.getId(), definition);
        }
    }

    protected void initializeCurrentValues() {
        if (ExplorerApp.get().getCrystalBallSimulationDebugger() != null) {
            this.simulationDebugger = ExplorerApp.get().getCrystalBallSimulationDebugger();
            this.simulationEvents = ExplorerApp.get().getCrystalBallSimulationEvents();

            String selectedDefinitionId = ExplorerApp.get().getCrystalBallCurrentDefinitionId();
            if (selectedDefinitionId != null) {
                definitionSelect.setValue(selectedDefinitionId);
            }

            String selectedInstanceId = ExplorerApp.get().getCrystalBallCurrentInstanceId();
            if (selectedInstanceId != null) {
                instanceTable.setValue(selectedInstanceId);
            }

            List<HistoricProcessInstance> replayProcessInstanceList = historyService.createHistoricProcessInstanceQuery()
                    .processInstanceBusinessKey(SIMULATION_BUSINESS_KEY)
                    .orderByProcessInstanceStartTime()
                    .desc()
                    .list();
            if (replayProcessInstanceList != null && !replayProcessInstanceList.isEmpty()) {
                replayHistoricInstance = replayProcessInstanceList.get(0);
            }

            refreshEvents();
        }
    }

    protected void init() {
        setSizeFull();
        addStyleName(Reindeer.PANEL_LIGHT);

        initProcessInstances();
        initEvents();
    }

    protected void initProcessInstances() {
        HorizontalLayout instancesHeader = new HorizontalLayout();
        instancesHeader.setSpacing(false);
        instancesHeader.setMargin(false);
        instancesHeader.setWidth(100, UNITS_PERCENTAGE);
        instancesHeader.addStyleName(ExplorerLayout.STYLE_DETAIL_BLOCK);
        addDetailComponent(instancesHeader);

        initProcessInstanceTitle(instancesHeader);

        HorizontalLayout selectLayout = new HorizontalLayout();
        selectLayout.setSpacing(true);
        selectLayout.setMargin(true);
        selectLayout.setWidth(50, UNITS_PERCENTAGE);
        addDetailComponent(selectLayout);

        definitionSelect = new NativeSelect(i18nManager.getMessage(Messages.DEPLOYMENT_HEADER_DEFINITIONS));
        definitionSelect.setImmediate(true);
        for (ProcessDefinition definition : definitionList) {
            definitionSelect.addItem(definition.getId());
            definitionSelect.setItemCaption(definition.getId(), definition.getName());
        }
        definitionSelect.addListener(new ValueChangeListener() {

            private static final long serialVersionUID = 1L;

            @Override
            public void valueChange(ValueChangeEvent event) {
                if (definitionSelect.getValue() != null) {
                    String selectedDefinitionId = (String) definitionSelect.getValue();
                    ExplorerApp.get().setCrystalBallCurrentDefinitionId(selectedDefinitionId);
                    refreshInstances(selectedDefinitionId);
                }
            }
        });

        selectLayout.addComponent(definitionSelect);

        replayButton = new Button(i18nManager.getMessage(Messages.CRYSTALBALL_BUTTON_REPLAY));
        replayButton.setEnabled(false);
        replayButton.addListener(new ClickListener() {

            private static final long serialVersionUID = 1L;

            @Override
            public void buttonClick(ClickEvent event) {
                if (instanceTable.getValue() != null) {
                    String processInstanceId = (String) instanceTable.getValue();
                    ExplorerApp.get().setCrystalBallCurrentInstanceId(processInstanceId);
                    List<EventLogEntry> eventLogEntries = managementService.getEventLogEntriesByProcessInstanceId(processInstanceId);
                    if (eventLogEntries == null || eventLogEntries.isEmpty()) return;
                    EventLogTransformer transformer = new EventLogTransformer(getTransformers());
                    simulationEvents = transformer.transform(eventLogEntries);
                    ExplorerApp.get().setCrystalBallSimulationEvents(simulationEvents);

                    SimpleEventCalendar eventCalendar = new SimpleEventCalendar(
                            ProcessEngines.getDefaultProcessEngine().getProcessEngineConfiguration().getClock(),
                            new SimulationEventComparator());
                    eventCalendar.addEvents(simulationEvents);

                    // replay process instance run
                    simulationDebugger = new ReplaySimulationRun(ProcessEngines.getDefaultProcessEngine(),
                            eventCalendar, getReplayHandlers(processInstanceId));
                    ExplorerApp.get().setCrystalBallSimulationDebugger(simulationDebugger);

                    simulationDebugger.init(new NoExecutionVariableScope());

                    simulationDebugger.step();

                    // replay process was started
                    List<HistoricProcessInstance> replayProcessInstanceList = historyService.createHistoricProcessInstanceQuery()
                            .processInstanceBusinessKey(SIMULATION_BUSINESS_KEY)
                            .orderByProcessInstanceStartTime()
                            .desc()
                            .list();
                    if (replayProcessInstanceList != null && !replayProcessInstanceList.isEmpty()) {
                        replayHistoricInstance = replayProcessInstanceList.get(0);
                    }

                    refreshEvents();
                }
            }
        });
        selectLayout.addComponent(replayButton);
        selectLayout.setComponentAlignment(replayButton, Alignment.MIDDLE_LEFT);

        instanceLayout = new HorizontalLayout();
        instanceLayout.setWidth(100, UNITS_PERCENTAGE);
        addDetailComponent(instanceLayout);

        initInstancesTable();
    }

    protected void initProcessInstanceTitle(HorizontalLayout instancesHeader) {
        Label titleHeader = new Label(i18nManager.getMessage(Messages.PROCESS_INSTANCES));
        titleHeader.addStyleName(ExplorerLayout.STYLE_H3);
        instancesHeader.addComponent(titleHeader);
    }

    protected void initInstancesTable() {
        if (instanceList == null || instanceList.isEmpty()) {
            noMembersTable = new Label(i18nManager.getMessage(Messages.ADMIN_RUNNING_NONE_FOUND));
            instanceLayout.addComponent(noMembersTable);

        } else {

            instanceTable = new Table();
            instanceTable.setWidth(100, UNITS_PERCENTAGE);
            instanceTable.setHeight(200, UNITS_PIXELS);

            instanceTable.setEditable(false);
            instanceTable.setImmediate(true);
            instanceTable.setSelectable(true);
            instanceTable.setSortDisabled(false);

            instanceTable.addContainerProperty("id", String.class, null, i18nManager.getMessage(Messages.PROCESS_INSTANCE_ID), null, Table.ALIGN_LEFT);
            instanceTable.addContainerProperty("definitionName", String.class, null, i18nManager.getMessage(Messages.PROCESS_INSTANCE_NAME), null, Table.ALIGN_LEFT);
            instanceTable.addContainerProperty("started", String.class, null, i18nManager.getMessage(Messages.PROCESS_INSTANCE_STARTED), null, Table.ALIGN_LEFT);
            instanceTable.addContainerProperty("ended", String.class, null, i18nManager.getMessage(Messages.PROCESS_INSTANCE_ENDED), null, Table.ALIGN_LEFT);

            fillInstanceValues();

            instanceTable.addListener(new Property.ValueChangeListener() {
                private static final long serialVersionUID = 1L;

                public void valueChange(ValueChangeEvent event) {
                    Item item = instanceTable.getItem(event.getProperty().getValue());
                    if (item != null) {
                        replayButton.setEnabled(true);
                    } else {
                        replayButton.setEnabled(false);
                    }
                }
            });

            instanceLayout.addComponent(instanceTable);
        }
    }

    protected void refreshInstances(String processDefinitionId) {
        instanceList = historyService.createHistoricProcessInstanceQuery()
                .processDefinitionId(processDefinitionId)
                .orderByProcessInstanceStartTime()
                .desc()
                .list();
        instanceTable.removeAllItems();
        fillInstanceValues();
    }

    protected void fillInstanceValues() {
        for (HistoricProcessInstance processInstance : instanceList) {
            ProcessDefinition definition = definitionMap.get(processInstance.getProcessDefinitionId());
            String definitionName = "";
            if (definition != null) {
                if (definition.getName() != null) {
                    definitionName = definition.getName();
                } else {
                    definitionName = definition.getId();
                }

                definitionName += " (v" + definition.getVersion() + ")";
            }

            instanceTable.addItem(new String[]{processInstance.getId(),
                    definitionName, processInstance.getStartTime().toString(),
                    processInstance.getEndTime() != null ? processInstance.getEndTime().toString() : ""}, processInstance.getId());
        }
    }

    protected void initEvents() {
        HorizontalLayout eventsHeader = new HorizontalLayout();
        eventsHeader.setSpacing(true);
        eventsHeader.setWidth(80, UNITS_PERCENTAGE);
        eventsHeader.addStyleName(ExplorerLayout.STYLE_DETAIL_BLOCK);
        addDetailComponent(eventsHeader);

        initEventTitle(eventsHeader);

        stepButton = new Button(i18nManager.getMessage(Messages.CRYSTALBALL_BUTTON_NEXTEVENT));
        stepButton.setEnabled(false);
        stepButton.addListener(new ClickListener() {
            private static final long serialVersionUID = 1L;

            @Override
            public void buttonClick(ClickEvent event) {
                if (!SimulationRunContext.getEventCalendar().getEvents().isEmpty()) {
                    simulationDebugger.step();
                    refreshEvents();
                }
            }
        });
        eventsHeader.addComponent(stepButton);
        eventsHeader.setComponentAlignment(stepButton, Alignment.MIDDLE_LEFT);

        showProcessInstanceButton = new Button();
        showProcessInstanceButton.addStyleName(Reindeer.BUTTON_LINK);
        showProcessInstanceButton.addListener(new ClickListener() {
            private static final long serialVersionUID = 1L;

            public void buttonClick(ClickEvent event) {
                if (replayHistoricInstance != null) {
                    ExplorerApp.get().getViewManager().showMyProcessInstancesPage(replayHistoricInstance.getId());
                }
            }
        });

        eventsHeader.addComponent(showProcessInstanceButton);
        eventsHeader.setComponentAlignment(showProcessInstanceButton, Alignment.MIDDLE_LEFT);

        eventLayout = new HorizontalLayout();
        eventLayout.setWidth(100, UNITS_PERCENTAGE);
        addDetailComponent(eventLayout);
        initEventsTable();
    }

    protected void initEventTitle(HorizontalLayout eventsHeader) {
        Label usersHeader = new Label(i18nManager.getMessage(Messages.ADMIN_DEFINITIONS));
        usersHeader.addStyleName(ExplorerLayout.STYLE_H3);
        eventsHeader.addComponent(usersHeader);
    }

    protected void initEventsTable() {
        eventTable = new Table();
        eventTable.setVisible(false);
        eventTable.setWidth(100, UNITS_PERCENTAGE);
        eventTable.setHeight(250, UNITS_PIXELS);

        eventTable.setEditable(false);
        eventTable.setImmediate(true);
        eventTable.setSelectable(true);
        eventTable.setSortDisabled(false);

        eventTable.addContainerProperty("type", String.class, null, i18nManager.getMessage(Messages.CRYSTALBALL_EVENT_TYPE), null, Table.ALIGN_LEFT);
        eventTable.addContainerProperty("executed", String.class, null, i18nManager.getMessage(Messages.CRYSTALBALL_EVENT_EXECUTED), null, Table.ALIGN_LEFT);

        eventLayout.addComponent(eventTable);
    }

    protected void refreshEvents() {
        stepButton.setEnabled(false);
        showProcessInstanceButton.setVisible(false);
        eventTable.removeAllItems();
        fillEventValues();
    }

    protected void fillEventValues() {
        for (SimulationEvent originalEvent : simulationEvents) {
            boolean executed = true;
            if (SimulationRunContext.getEventCalendar() != null && SimulationRunContext.getEventCalendar().getEvents() != null) {
                for (SimulationEvent event : SimulationRunContext.getEventCalendar().getEvents()) {
                    if (originalEvent.equals(event)) {
                        executed = false;
                        stepButton.setEnabled(true);
                        break;
                    }
                }
            }

            Object itemId = eventTable.addItem();
            eventTable.getItem(itemId).getItemProperty("type").setValue(originalEvent.getType());
            eventTable.getItem(itemId).getItemProperty("executed").setValue(executed);
        }

        if (replayHistoricInstance != null && replayHistoricInstance.getId() != null) {
            ProcessInstance testInstance = runtimeService.createProcessInstanceQuery().processInstanceId(replayHistoricInstance.getId()).singleResult();
            if (testInstance != null) {
                showProcessInstanceButton.setCaption(i18nManager.getMessage(
                        Messages.TASK_PART_OF_PROCESS, definitionMap.get(replayHistoricInstance.getProcessDefinitionId())));
                showProcessInstanceButton.setVisible(true);
            }
        }

        eventTable.setVisible(true);
    }

    protected List<Function<EventLogEntry, SimulationEvent>> getTransformers() {
        List<Function<EventLogEntry, SimulationEvent>> transformers = new ArrayList<Function<EventLogEntry, SimulationEvent>>();
        transformers.add(new EventLogProcessInstanceCreateTransformer(PROCESS_INSTANCE_START_EVENT_TYPE, PROCESS_DEFINITION_ID_KEY, SIMULATION_BUSINESS_KEY, VARIABLES_KEY));
        transformers.add(new EventLogUserTaskCompleteTransformer(USER_TASK_COMPLETED_EVENT_TYPE));
        return transformers;
    }

    protected Map<String, SimulationEventHandler> getReplayHandlers(String processInstanceId) {
        Map<String, SimulationEventHandler> handlers = new HashMap<String, SimulationEventHandler>();
        handlers.put(PROCESS_INSTANCE_START_EVENT_TYPE, new StartReplayLogEventHandler(processInstanceId, PROCESS_DEFINITION_ID_KEY, SIMULATION_BUSINESS_KEY, VARIABLES_KEY));
        handlers.put(USER_TASK_COMPLETED_EVENT_TYPE, new ReplayUserTaskCompleteEventHandler());
        return handlers;
    }
}
